﻿/*
 * This is part of the dotDiff Project
 * 
 * Author: David Rundle (http://www.dbones.co.uk)
 * Project Home: http://www.codeplex.com/DotDiff
 * 
 */

#region Using

using System;
using System.Collections.Generic;
using System.Xml;

#endregion

namespace DotDiff
{
    /// <summary>
    /// simple class which will try to contain the information
    /// required to compare 2 XML files. (well one agaist the other)
    /// </summary>
    public class XmlCompare
    {

        #region Field Members

        #region Variable Members

        private List<DiffInstance> differences;     //store a reference of all the differences.
        private XmlDocument newDoc;                 //one with changes
        private XmlDocument oldDoc;                 //origonal
        private List<DiffInstance> toRemove;        //this will list all the instances which are ready to delete, use after a foreach
        private XmlFunctions xmlFunctions;          //instance of the Xml Functions
        private XmlSettings xmlSettings;            //these are the Xml settings to use on this comparison class.

        #endregion

        #endregion

        #region Ctors

        /// <summary>
        /// Create an instance of the compairer.
        /// </summary>
        public XmlCompare()
        {
            
            oldDoc = new XmlDocument();
            newDoc = new XmlDocument();
            differences = new List<DiffInstance>();
            toRemove = new List<DiffInstance>();
        }

        #endregion

        #region Property Members

        #region Differences

        /// <summary>
        /// this is a list of the differences between the 2 documents
        /// </summary>
        public List<DiffInstance> Differences
        {
            get
            {
                return this.differences;
            }
        }

        #endregion

        //#region IdNode

        ///// <summary>
        ///// denotes a node as the ID node.
        ///// </summary>
        //public string IdNode
        //{
        //    get { return idNode; }
        //    set
        //    {
        //        hasIdNode = !string.IsNullOrEmpty(value);
        //        idNode = string.IsNullOrEmpty(value) ? "" : value;
        //        if (xmlFunctions != null)
        //        {
        //            xmlFunctions.XmlSettings.IdNode = idNode;
        //        }
        //    }
        //}

        //#endregion

        #region NewDoc

        /// <summary>
        /// this document contains any changes (could be the
        /// complete document or just the change graph)
        /// </summary>
        public XmlDocument NewDoc
        {
            get
            {
                return this.newDoc;
            }
        }

        #endregion

        #region NsMgr

        ///// <summary>
        ///// The namespace manager to use during the comparison, this will affect any
        /////  Xpath generated.
        ///// </summary>
        //public XmlNamespaceManager NsMgr
        //{
        //    get
        //    {
        //        return this.nsMgr;
        //    }
        //    set
        //    {
        //        this.nsMgr = value;
        //    }
        //}

        #endregion

        #region OldDoc

        /// <summary>
        /// the original document
        /// </summary>
        public XmlDocument OldDoc
        {
            get
            {
                return this.oldDoc;
            }
        }

        #endregion

        #region XmlFunctions

        /// <summary>
        /// This is the instance of the XmlFunctions, this will have useful functions for UI manipulation
        /// </summary>
        public XmlFunctions XmlFunctions
        {
            get
            {
                return this.xmlFunctions;
            }
            set
            {
                this.xmlFunctions = value;
            }
        }

        #endregion

        /// <summary>
        /// The XmlSettings used to compare the XML documents
        /// </summary>
        public XmlSettings XmlSettings
        {
            get
            {
                return this.xmlSettings;
            }
            set
            {
                this.xmlSettings = value;
            }
        }

        #endregion

        #region Method Members

        #region Public Methods

        #region Compare ()

        /// <summary>
        /// This will compare both documents
        /// and populate any difference into the Diff List
        /// </summary>
        /// <remarks>Reconmend added the Namespace manager</remarks>
        public void Compare()
        {
            //check to see if we have 2 files to compaire
            if (newDoc == null)
            {
                throw new NullReferenceException("The New Doc has to be referenced, please load it in");
            }
            if (oldDoc == null)
            {
                throw new NullReferenceException("The Old Doc has to be referenced, please load it in");
            }

            //Clear the diff list
            differences.Clear();
            toRemove.Clear();

            //what do we do to find a difference
            //Skip the Root node.
            //get the first element in the NewDoc and try to find it in the OldDoc

            //Object, an Xml node which holds child nodes.
            //Find the Object, if cannot be found, then its missing

            //if current then, cycle through child Values or Objects.

            //see if there are some XmlSettings
            if (xmlSettings == null)
            {
                xmlSettings = new XmlSettings();
                
            }

            //this is possible. check the nsMgr
            if (xmlSettings.NsMgr == null)
            {
                xmlSettings.NsMgr = new XmlNamespaceManager(oldDoc.NameTable);
            }
            
            if (xmlFunctions == null)
            {
                xmlFunctions = new XmlFunctions(xmlSettings);
            }

            Compare(newDoc.DocumentElement);

        }

        #endregion

        #region CompareFromNode

        /// <summary>
        /// Start the comparison from a child not instead of the root node
        /// </summary>
        /// <param name="nodeToComapreFrom">the node to start comparing from</param>
        public void CompareFromNode(XmlNode nodeToComapreFrom)
        {
            //Clear the diff list
            differences.Clear();
            Compare(nodeToComapreFrom);
        }

        #endregion

        #region GenerateDifferenceGraph

        /// <summary>
        /// this will create an XML document with only the changes 
        /// please note this will not work with XPath node Posistioning.
        /// </summary>
        /// <returns>an XmlDoc of the changes</returns>
        public XmlDocument GenerateDifferenceGraph()
        {
            //make sure the compare engine has been run.
            if (differences.Count == 0)
            {
                Compare();
            }

            XmlNamespaceManager nsMgr = xmlSettings.NsMgr;
            string idNode = xmlSettings.IdNode;
            bool hasIdNode = string.IsNullOrEmpty(idNode);
            
            XmlDocument diffGraph = new XmlDocument(nsMgr.NameTable);

            //foreach change generate all the required nodes to store it.
            foreach (DiffInstance instance in differences)
            {

                //this will build a skeleton node structure (all parent nodes upto the instance node)
                XmlFunctions.CopyNode(instance.NewXml, diffGraph);

                //if the node difference was an id, then there is no need
                //to copy it over here, as the CopyNode just did it.
                if (hasIdNode && !instance.XPath.EndsWith(idNode) || !hasIdNode)
                {
                    //now add the node to the structure
                    string parentXPath = XmlFunctions.GetParentXPath(instance.XPath);
                    XmlNode parent = diffGraph.SelectSingleNode(parentXPath, nsMgr);
                    XmlFunctions.CopyEntireNode(instance.NewXml, parent);
                }

            }

            return diffGraph;
        }

        #endregion

        #region LoadNewDocXml

        /// <summary>
        /// load the xml into the NewXml document
        /// </summary>
        /// <param name="xml"></param>
        public void LoadNewDocXml(string xml)
        {
            newDoc.LoadXml(xml);
        }

        #endregion

        #region LoadOldDocXml

        /// <summary>
        /// Load the XML into the OldXml Docuemnt
        /// </summary>
        /// <param name="xml"></param>
        public void LoadOldDocXml(string xml)
        {
            oldDoc.LoadXml(xml);
        }

        #endregion

        #region MergeAll

        /// <summary>
        /// Merges all the differences from the new doc to the old
        /// </summary>
        public void MergeAll()
        {
            foreach (DiffInstance diffInstance in differences)
            {
                MergeNode(diffInstance);
            }
            differences.Clear();
        }

        #endregion

        #region MergeNode

        /// <summary>
        /// merges from NewDoc to the OldDoc
        /// </summary>
        /// <param name="diffInstance">the difference instance.</param>
        public void MergeNode(DiffInstance diffInstance)
        {
            if (diffInstance.OldXml == null)
            {
                string parentXPath = XmlFunctions.GetParentXPath(diffInstance.XPath);
                XmlNode parent = oldDoc.SelectSingleNode(parentXPath, xmlSettings.NsMgr);
                XmlFunctions.CopyEntireNode(diffInstance.NewXml, parent);
            }
            else
            {
                //clean out the attributes, and copy over the new ones
                diffInstance.OldXml.Attributes.RemoveAll();
                XmlFunctions.CopyAttributes(diffInstance.NewXml, diffInstance.OldXml);

                //copy accross the value.
                diffInstance.OldXml.InnerText = diffInstance.NewXml.InnerText;
            }
            //update the list
            toRemove.Add(diffInstance);

        }

        #endregion

        #region UpdateDifferences

        /// <summary>
        /// after mearging changes in a foreach loop run this, and it will remove any mearge change from the differences list
        /// </summary>
        public void UpdateDifferences()
        {
            foreach (DiffInstance diffInstance in toRemove)
            {
                differences.Remove(diffInstance);
            }
            toRemove.Clear();
        }

        #endregion

        #endregion

        #region Private Methods

        /// <summary>
        /// this is the compair engine. private, as its the main process engine.
        /// </summary>
        /// <param name="node">the new documents node to compare</param>
        private void Compare(XmlNode node)
        {
            //only process the elements
            if (node.GetType() != typeof(XmlElement))
                return;

            XmlNode oldDocNode = null;
            bool identical = false;

            //look for difference
            identical = XmlFunctions.IsValue(node) ? FindValue(node, ref oldDocNode) : FindObject(node);

            //see if this item was ok?
            if (!identical)
            {
                //log it as a difference.
                DiffInstance diff = new DiffInstance();
                diff.NewXml = node;
                diff.OldXml = oldDocNode;
                diff.XPath = XmlFunctions.GetXPath(node);
                diff.Difference = (oldDocNode == null) ? DiffType.Missing : DiffType.Different;
                differences.Add(diff);
            }
            //do not process the child elements if the parent did not exist
            else
            {
                foreach (XmlNode childNode in node.ChildNodes)
                {
                    Compare(childNode);
                }
            }
        }

        /// <summary>
        /// returns the Object, this is an XML Node, which is classifed as an object.
        /// </summary>
        /// <param name="objectToFind">The newXml's node, which is a Object node.</param>
        /// <returns>corrisponding proj node</returns>
        private bool FindObject(XmlNode objectToFind)
        {
            //all you have to care about is the Element name, and any attributes.
            if (!XmlFunctions.IsObject(objectToFind))
                return false;
            string xpath = XmlFunctions.GetXPath(objectToFind);

            //must find the node.
            XmlNode found = oldDoc.SelectSingleNode(xpath, xmlSettings.NsMgr);

            return found != null;
        }

        /// <summary>
        /// returns the proj value element.
        /// </summary>
        /// <param name="valueToFind">node to look for</param>
        /// <param name="foundValue">this is the proj value, treat is as a ref, it will be assigned if it exists</param>
        /// <returns>are they equal?</returns>
        private bool FindValue(XmlNode valueToFind, ref XmlNode foundValue)
        {
            //only find the node... as the value does not matter, its the location!!
            if (!XmlFunctions.IsValue(valueToFind))
                return false;
            string xpath = XmlFunctions.GetXPath(valueToFind);
            foundValue = oldDoc.SelectSingleNode(xpath, xmlSettings.NsMgr);

            if (foundValue == null)
                return false;
            if (foundValue.InnerText != valueToFind.InnerText)
                return false;

            return true;
        }

        #endregion

        #endregion

    }

}
